// ==UserScript==
// @name SuperK - 键盘刷视频(受控端)
// @namespace https://scriptcat.org/zh-CN/script-show-page/3372
// @version 0.2
// @description 控制端监听按键,受控端接收按键消息后控制网页内容。用法举例:打游戏时在不切屏的情况下控制抖音网页端的上一个视频,下一个视频,播放和停止。
// @author beibeibeibei
// @license MIT
// @match https://www.douyin.com/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAdZJREFUeF7tmk1OwzAQhWeg7FFPkEoUiVtwlHaZXgJ6Cbpsb8IxkACpOUHEnipGgU1rBM+OQf7J6/b5Z+abN04qR2XkPx15/kIAdMDICbAFRm4AHoJsAbbAyAmwBVwMcLl5qs7k4k5MdyuilcucdMaYxog2Rg/Lt/qmseOCDvhM3kz26SQ0PJJODzMbAgQw3bxuxZjF8G0Tmqly39bz9XFEGMDDi0koheBQ2tX8JGdvAPYCwRH98wJTq4AEQAewBf72DEA9lroefAimniCKjwDQUwgRzF0PdgACGFtHBSIAVCFEEM2PraP46QBUIUTQ1u317Hdv3/XQ/ND9gh1AAODvMqpgqE4HWAR8gQa3ADpDYuvozCEAVCFEEM2PraP46QBUIUTQV0f7Id33kEPxBTsAbYDeE1DCoY85FB8BoAoggr462g/pybUACji2jgoU3AKxE0T7EwBvhk7vNgdcjT3v8/sm4KfGME27up4dq/gMKOp6XHdtfbX0AvD1gcT5Y/4u+F79HgR0QD+oh6BmslUxVZYgVHedvK8HfSKDHjO5604OyD3J3+IngJKr65IbHeBCqeQxdEDJ1XXJjQ5woVTyGDqg5Oq65EYHuFAqeQwdUHJ1XXL7AKF/3lBxkE7hAAAAAElFTkSuQmCC
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// 添加样式
function add_css() {
GM_addStyle(`
.beibeibeibei-panel-warpper {
width: 310px;
height: 190px;
position: fixed;
top: 0;
left: 0;
display: flex;
transition-timing-function: linear(0 0%, 0 1.8%, 0.01 3.6%, 0.03 6.35%, 0.07 9.1%, 0.13 11.4%, 0.19 13.4%, 0.27 15%, 0.34 16.1%, 0.54 18.35%, 0.66 20.6%, 0.72 22.4%, 0.77 24.6%, 0.81 27.3%, 0.85 30.4%, 0.88 35.1%, 0.92 40.6%, 0.94 47.2%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%);
transition-duration: .3s;
z-index: 1061;
}
.beibeibeibei-panel-warpper svg {
vertical-align: initial;
}
.corner-box-warpper {
.corner-box {
background: transparent;
opacity: 0;
width: 310px;
height: 190px;
position: fixed;
pointer-events: none;
}
.topright {
z-index: -99999;
top: 0;
right: 0;
}
.bottomright {
z-index: -99999;
bottom: 0;
right: 0;
}
.bottomleft {
z-index: -99999;
bottom: 0;
left: 0;
}
.topleft {
z-index: -99999;
top: 0;
left: 0;
}
}
.beibeibeibei-panel-border {
background: black;
width: fit-content;
height: fit-content;
border-radius: 6px;
}
@keyframes movebackground {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.beibeibeibei-panel-border.running {
background: linear-gradient(45deg, rgb(84 58 181), rgb(84 58 181 / 70%), rgb(0 172 193 / 70%), rgb(87 228 246), rgb(255 255 255));
animation: movebackground 4s infinite linear;
background-size: 400%;
}
#beibeibeibei-panel {
box-sizing: content-box;
width: 300px;
height: 180px;
display: flex;
flex-direction: column;
background: black;
padding: 2px;
border: 1px solid white;
border-radius: 6px;
overflow: hidden;
interpolate-size: allow-keywords;
margin: 2px;
transition-duration: 0.3s;
transition-timing-function: linear(0 0%, 0 1.8%, 0.01 3.6%, 0.03 6.35%, 0.07 9.1%, 0.13 11.4%, 0.19 13.4%, 0.27 15%, 0.34 16.1%, 0.54 18.35%, 0.66 20.6%, 0.72 22.4%, 0.77 24.6%, 0.81 27.3%, 0.85 30.4%, 0.88 35.1%, 0.92 40.6%, 0.94 47.2%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%);
}
#beibeibeibei-panel .titlebar {
display: flex;
justify-content: flex-end;
-webkit-user-select: none;
user-select: none;
container-name: titlebar;
container-type: inline-size;
}
#beibeibeibei-panel .titlebar>* {
width: 22px;
height: 22px;
padding: 0;
overflow: hidden;
}
#beibeibeibei-panel .titlebar>*:not(:last-child) {
margin-right: 2px;
}
@container titlebar (max-width: 120px) {
/* 父容器宽度不够时不显示 */
#beibeibeibei-panel .titlebar>*:not(:last-child) {
display: none;
}
}
#beibeibeibei-panel .titlebar>button {
cursor: pointer;
}
#beibeibeibei-panel .titlebar>.led {
border: 1px solid #767676;
background-color: #efefef;
border-radius: 3px;
box-sizing: border-box;
cursor: help;
overflow: hidden;
}
@supports (rx: 0) {
#beibeibeibei-panel .titlebar>.led ellipse {
transform-origin: 10px 10px;
fill: gray;
rx: 7;
ry: 1;
transform: rotate(120deg);
transition: all 0.3s ease-out;
}
}
@supports not (rx: 0) {
#beibeibeibei-panel .titlebar>.led ellipse {
transform-origin: 10px 10px;
fill: gray;
transform: rotate(120deg) scaleX(1.4) scaleY(0.2);
transition: all 0.3s ease-out;
}
}
@keyframes flicker {
0%,
100% {
transform: rotate(120deg) scale(1);
}
50% {
transform: rotate(120deg) scale(1.2);
}
}
@supports (rx: 0) {
#beibeibeibei-panel .titlebar>.led.websocket-connected ellipse {
animation: flicker 1.5s infinite;
fill: #44ff00;
rx: 5;
ry: 5;
}
}
@supports not (rx: 0) {
#beibeibeibei-panel .titlebar>.led.websocket-connected ellipse {
animation: flicker 1.5s infinite;
fill: #44ff00;
}
}
#beibeibeibei-panel .titlebar>.min .vline {
opacity: 0;
}
#beibeibeibei-panel .msgs {
flex-grow: 1;
margin: 4px 0px;
background: #2d2d2d;
overflow-y: auto;
}
#beibeibeibei-panel .msgs .info,
#beibeibeibei-panel .msgs .msg,
#beibeibeibei-panel .msgs .error {
color: #efefef;
font-size: 9px;
margin: 0;
}
#beibeibeibei-panel .msgs .error {
color: #87ceeb;
}
#beibeibeibei-panel .footer {
display: flex;
height: 30px;
min-height: 30px;
justify-content: space-between;
}
#beibeibeibei-panel .footer>* {
width: calc(50% - 2px);
box-sizing: border-box;
text-align: center;
font-family: Arial, sans-serif;
}
#beibeibeibei-panel .footer>input {
background: white;
border: 2px solid gray;
border-radius: 4px;
}
#beibeibeibei-panel .footer>.start {
cursor: pointer;
-webkit-user-select: none;
user-select: none;
white-space: nowrap;
}
/*隐藏输入框箭头*/
.beibeibeibei-panel-warpper input::-webkit-outer-spin-button,
.beibeibeibei-panel-warpper input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
@supports (-moz-appearance:none) {
@-moz-document url-prefix() {
.beibeibeibei-panel-warpper input[type="number"] {
-moz-appearance: textfield;
appearance: none;
}
}
}
#beibeibeibei-panel-rule-dialog {
position: fixed;
top: 50%;
transform: translateY(round(-50%, 1px));
border: none;
border-radius: 6px;
margin: 0 auto;
padding: 0;
box-shadow: 1px 1px 5px 2px white;
&::backdrop {
/* Safari 9+ */
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
}
.title {
width: 100%;
height: 31px;
background-color: black;
display: flex;
flex-direction: row-reverse;
border-radius: 6px 6px 0 0;
border-bottom: 1px solid #555;
.close {
width: 46px;
height: 100%;
background-color: black;
color: white;
border-radius: 0 6px 0 0;
border: none;
cursor: pointer;
outline: none;
&:focus {
border: 1px solid white;
}
svg {
transition: all 0.1s;
line {
transition: all 0.1s;
transform-origin: center;
}
}
&:hover {
background-color: #0F0000;
svg {
transform: rotate(90deg);
}
}
&:active {
border: 1px solid darkred;
svg {
line {
transform: rotate(90deg);
stroke: red;
}
}
}
}
}
.table {
min-width: 180px;
min-height: 100px;
background-color: #222;
color: white;
border-radius: 0 0 6px 6px;
padding: 10px;
font-size: 12px;
.column {
display: flex;
height: 40px;
background-color: black;
>* {
width: 100px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
>*:not(:last-child) {
border-right: 1px solid gray;
}
}
.column.header {
border-radius: 6px 6px 0 0;
border: 1px solid gray;
>*:last-of-type {
/*不选中规则删除这个格子*/
-webkit-user-select: none;
user-select: none;
}
}
.column.body {
border: 1px solid gray;
border-top: none;
button.remove {
background-color: #111;
color: white;
border: 1px solid white;
border-radius: 4px;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
&:hover {
border-color: #BBB;
box-shadow: inset 0px 0px 2px 0px white;
}
&:active {
border-color: #888;
color: #888;
transform: scale(0.95);
}
}
}
.column:last-of-type {
border-radius: 0 0 6px 6px;
}
.add {
width: 100%;
height: 40px;
cursor: pointer;
background-color: black;
color: white;
border: 1px solid white;
border-radius: 6px;
margin-top: 4px;
-webkit-user-select: none;
user-select: none;
&:hover {
box-shadow: inset 0px 0px 2px 0px white;
}
&:active {
transform: translate(1px, 1px);
width: calc(100% - 2px);
}
}
}
}
#beibeibeibei-panel-add-rule-dialog {
position: fixed;
top: 50%;
transform: translateY(round(-50%, 1px));
border: none;
border-radius: 6px;
margin: 0 auto;
padding: 0;
box-shadow: 0px 0px 5px 0px white;
background-color: black;
width: 470px;
overflow: hidden;
&::backdrop {
/* Safari 9+ */
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
}
.title {
width: 100%;
height: 31px;
background-color: black;
display: flex;
flex-direction: row-reverse;
border-bottom: 1px solid #555;
.close {
width: 46px;
height: 100%;
background-color: black;
color: white;
border-radius: 0 6px 0 0;
border: none;
cursor: pointer;
outline: none;
&:focus {
border: 1px solid white;
}
svg {
transition: all 0.1s;
line {
transition: all 0.1s;
transform-origin: center;
}
}
&:hover {
background-color: #0F0000;
svg {
transform: rotate(90deg);
}
}
&:active {
border: 1px solid darkred;
svg {
line {
transform: rotate(90deg);
stroke: red;
}
}
}
}
}
.add-panel {
background-color: #222;
color: white;
padding: 10px;
font-size: 12px;
.add-panel-item {
margin-bottom: 10px;
input {
background-color: #2d3436;
color: #ecf0f1;
border: 1px solid #34495e;
border-radius: 4px;
font-size: 12px;
padding: 1px 3px;
outline: none;
text-align: center;
vertical-align: top;
&:focus {
border: 1px solid white;
}
}
input[type="number"] {
width: 30px;
}
}
.add-panel-item.text-center {
text-align: center;
}
}
.footer {
display: flex;
justify-content: space-around;
padding: 10px;
border-top: 1px solid #555;
background: black;
-webkit-user-select: none;
user-select: none;
button {
background-color: #2d3436;
color: #ecf0f1;
border: 1px solid #34495e;
border-radius: 4px;
font-size: 12px;
padding: 6px 12px;
cursor: pointer;
outline: none;
transition: background-color 0.2s, transform 0.1s;
&:hover {
box-shadow: inset 0px 0px 5px 1px #3d4a51;
}
&:active {
transform: translate(1px, 1px);
background-color: #1a2023;
}
&:focus {
border: 1px solid white;
}
}
.add {
background-color: #05150b;
}
.cancel {
background-color: #170503;
}
}
}
/* 自定义select */
@keyframes custom-select-expand {
0% {
display: none;
height: 0px;
overflow-y: auto;
}
1% {
display: block;
overflow-y: hidden;
}
99% {
display: block;
overflow-y: hidden;
}
100% {
display: block;
height: auto;
overflow-y: auto;
}
}
.custom-select {
position: relative;
display: inline-block;
.select-button {
vertical-align: top;
background-color: #2d3436;
color: #ecf0f1;
border: 1px solid #34495e;
border-radius: 4px;
padding: 0px 3px;
padding-right: 20px;
font-size: 12px;
text-align: center;
background-repeat: no-repeat;
background-position: right 2px top 2px;
outline: none;
cursor: pointer;
}
.select-button:focus {
border: 1px solid white;
}
.options-container {
position: absolute;
left: 0;
width: 100%;
font-size: 12px;
background-color: #2d3436;
border: none;
border-radius: 4px;
overflow-y: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
white-space: nowrap;
height: 0px;
interpolate-size: allow-keywords;
}
.options-container::-webkit-scrollbar {
width: 6px;
background: #202324;
border-radius: 0 3px 3px 0;
}
.options-container::-webkit-scrollbar-thumb {
background: #454a4d;
border-radius: 3px;
}
.options-container::-webkit-scrollbar-thumb:hover {
background: #575e62;
}
.option-item {
background-color: #2d3436;
color: #ecf0f1;
padding: 4px 0px;
text-align: center;
cursor: pointer;
}
.option-item:hover {
background: #777;
}
.option-item:focus {
background: #777;
}
.option-item:focus-visible {
outline: none;
background: #777;
}
}
/* 选项向上展开 */
.custom-select.position-above {
.select-button {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='3,6 5,4 7,6'%20style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e");
}
.options-container {
bottom: calc(100% + 1px);
}
}
/* 选项向下展开 */
.custom-select.position-below {
.select-button {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='3,4 5,6 7,4'%20style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e");
}
.options-container {
top: calc(100% + 1px);
}
}
/* 选项右侧展开 */
.custom-select.position-right {
.select-button {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0 0 11 11'%3e%3cpolygon%20points='4,6 6,4 4,2' style='fill:white;stroke:white;stroke-width:1'/%3e%3c/svg%3e");
}
.options-container {
top: calc(50%);
left: calc(100% + 1px);
transform: translateY(-50%);
}
}
`);
}
// 动态插入 HTML 结构
function add_HTML() {
const panelWrapper = document.createElement('div');
panelWrapper.className = 'beibeibeibei-panel-warpper';
document.body.appendChild(panelWrapper);
panelWrapper.innerHTML = `
`;
}
function beibeibeibei_init() {
/* 关闭dialog1 */
document.querySelector("#beibeibeibei-panel-rule-dialog .title .close").addEventListener("click", (e) => {
e.target.closest('dialog').parentElement.querySelectorAll('dialog').forEach((d) => {
d.close();
})
});
/* 打开添加规则弹窗 */
document.querySelector("#beibeibeibei-panel-rule-dialog .table .add").addEventListener("click", (e) => {
document.querySelector('#beibeibeibei-panel-add-rule-dialog').showModal();
});
/* 关闭dialog2 title */
document.querySelector("#beibeibeibei-panel-add-rule-dialog .title .close").addEventListener("click", (e) => {
e.target.closest('dialog').close();
});
/* 关闭dialog2 返回*/
document.querySelector("#beibeibeibei-panel-add-rule-dialog .footer .cancel").addEventListener("click", (e) => {
e.target.closest('dialog').close();
});
/* 打开规则弹窗 */
document.querySelector("#beibeibeibei-panel .rule").addEventListener("click", () => {
/** @type {HTMLDialogElement} */
let dialog = document.querySelector("#beibeibeibei-panel-rule-dialog");
dialog.showModal();
});
/* 清空消息记录 */
document.querySelector("#beibeibeibei-panel .cls").addEventListener("click", () => {
let msgs = document.querySelector("#beibeibeibei-panel .msgs");
msgs.replaceChildren();
});
// #region /* 缩小面板 */
let panelIsMin = false;
document.querySelector("#beibeibeibei-panel .min").addEventListener("click", () => {
panelIsMin = !panelIsMin;
let vl = document.querySelector("#beibeibeibei-panel .min .vline");
vl.style.opacity = panelIsMin ? '1' : '0';
vl.parentElement.parentElement.title = panelIsMin ? "恢复面板" : "缩小面板";
let panel = document.querySelector("#beibeibeibei-panel");
panel.style.width = panelIsMin ? "22px" : "300px";
panel.style.height = panelIsMin ? "22px" : "180px";
});
// #endregion
// #region /* 移动位置 */
let panelpos = 3;
document.querySelector("#beibeibeibei-panel .move").addEventListener("click", () => {
panelpos = (panelpos + 1) % 4;
updatePos();
});
function updatePos() {
const wrapper = document.querySelector("#beibeibeibei-panel").parentElement.parentElement;
const positions = [getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .topright")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .bottomright")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .bottomleft")), getComputedStyle(document.querySelector(".beibeibeibei-panel-warpper .topleft"))];
wrapper.style.inset = positions[panelpos].inset;
switch (panelpos) {
case 0:
wrapper.style.alignItems = "flex-start";
wrapper.style.justifyContent = "flex-end";
break;
case 1:
wrapper.style.alignItems = "flex-end";
wrapper.style.justifyContent = "flex-end";
break;
case 2:
wrapper.style.alignItems = "flex-end";
wrapper.style.justifyContent = "flex-start";
break;
case 3:
wrapper.style.alignItems = "flex-start";
wrapper.style.justifyContent = "flex-start";
break;
}
};
const debounce = (fn, delay) => {
let timer;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn();
}, delay);
}
};
const cancalDebounce = debounce(updatePos, 200);
window.addEventListener('resize', cancalDebounce);
// #endregion
/* 按下 ESC 键时关闭弹窗 */
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
let dialog = document.querySelector("#beibeibeibei-panel-rule-dialog");
dialog.close();
let dialog2 = document.querySelector("#beibeibeibei-panel-add-rule-dialog");
dialog2.close();
}
});
// #region /* add-rule-dialog弹窗循环tab键功能 */
const focusStart = document.querySelector("#beibeibeibei-panel-add-rule-dialog > div.focus-start")
focusStart.addEventListener("focus", (event) => {
let els = document.querySelector('#beibeibeibei-panel-add-rule-dialog').querySelectorAll("input, select, button");
els[els.length - 1].focus();
});
const focusEnd = document.querySelector("#beibeibeibei-panel-add-rule-dialog > div.focus-end");
focusEnd.addEventListener("focus", (event) => {
let els = document.querySelector('#beibeibeibei-panel-add-rule-dialog').querySelectorAll("input, select, button");
els[0].focus();
});
// #endregion
// #region /* 根据选中的选项显示或隐藏对应的元素 */
const quantitySelector = document.querySelector('#beibeibeibei-panel-add-rule-dialog .quantitySelector');
const inputSingle = document.querySelector('#beibeibeibei-panel-add-rule-dialog .add-panel-item .single-input');
const inputRange = document.querySelector('#beibeibeibei-panel-add-rule-dialog .add-panel-item .range-input');
inputSingle.style.display = 'none';
inputRange.style.display = 'none';
quantitySelector.addEventListener('change', function () {
const selectedValue = this.querySelector('.select-button').getAttribute('data-value');
// 根据选择值显示对应输入
switch (selectedValue) {
case '1': // 无限制
default: // 其他情况隐藏所有输入
inputSingle.style.display = 'none';
inputRange.style.display = 'none';
break;
case '2': // 最少
case '3': // 最多
case '4': // 等于
inputSingle.style.display = 'inline';
inputRange.style.display = 'none';
break;
case '5': // 范围
inputSingle.style.display = 'none';
inputRange.style.display = 'inline';
break;
}
});
// #endregion
// #region /* 监听 dialog 关闭事件, 重置其中的内容 */
const dialog2 = document.querySelector('#beibeibeibei-panel-add-rule-dialog');
dialog2.addEventListener('close', () => {
// 重置
dialog2.querySelectorAll('input').forEach(input => {
input.value = '';
});
// 重置.custom-select组件
dialog2.querySelectorAll('.custom-select').forEach(customSelect => {
const selectButton = customSelect.querySelector('.select-button');
const optionsContainer = customSelect.querySelector('.options-container');
selectButton.textContent = '请选择';
selectButton.removeAttribute('data-value');
optionsContainer.style.animation = 'none';// 移除现有动画
void optionsContainer.offsetWidth;// 强制触发重绘以重置动画
});
// 隐藏动态输入区域
dialog2.querySelectorAll('.single-input, .range-input').forEach(el => {
el.style.display = 'none';
});
});
// #endregion
// #region /* 自定义select处理 */
document.querySelectorAll('.custom-select').forEach(customSelect => {
const selectButton = customSelect.querySelector('.select-button');
const optionsContainer = customSelect.querySelector('.options-container');
let isOpen = false;
selectButton.addEventListener('click', function (e) {
// 切换选项可见性
e.stopPropagation();
if (isOpen) {
resetAndPlay(optionsContainer, 'reverse');
customSelect.querySelectorAll('.option-item').forEach(option => {
option.setAttribute('tabindex', '-1');
})
} else {
resetAndPlay(optionsContainer, 'normal');
customSelect.querySelectorAll('.option-item').forEach(option => {
option.setAttribute('tabindex', '0');
})
}
isOpen = !isOpen;
});
customSelect.querySelectorAll('.option-item').forEach(option => {
// 点击事件处理
option.addEventListener('click', function () {
selectButton.textContent = this.textContent;
selectButton.setAttribute('data-value', this.dataset.value);
if (isOpen) {
resetAndPlay(optionsContainer, 'reverse');
isOpen = false;
}
// 触发自定义的 change 事件
const changeEvent = new Event('change', { bubbles: true });
customSelect.dispatchEvent(changeEvent);
});
// 键盘事件处理
option.addEventListener('keydown', function (e) {
switch (e.key) {
case 'Enter':
case ' ':{
e.preventDefault();
this.click();
selectButton.focus();
break;
}
case 'ArrowDown':{
e.preventDefault();
const next = this.nextElementSibling || optionsContainer.firstElementChild;
next.focus();
break;
}
case 'ArrowUp':{
e.preventDefault();
const prev = this.previousElementSibling || optionsContainer.lastElementChild;
prev.focus();
break;
}
case 'ArrowLeft':
case 'ArrowRight':{
// 按左键或右键时, 将焦点返回到select-button
e.preventDefault();
selectButton.focus();
break;
}
}
});
});
document.addEventListener('click', function () {
// 点击页面其他区域关闭选项
if (isOpen) {
resetAndPlay(optionsContainer, 'reverse');
isOpen = false;
}
});
optionsContainer.addEventListener('click', function (e) {
// 防止选项容器点击时冒泡
e.stopPropagation();
});
selectButton.addEventListener('keydown', function (e) {
switch (e.key) {
case 'ArrowLeft':
case 'ArrowRight':
// 按左键或右键时, 将焦点切换到option里
if (isOpen) {
e.preventDefault();
customSelect.querySelector('.option-item')?.focus();
}
break;
}
});
});
function resetAndPlay(el, direction) {
// 移除现有动画
el.style.animation = 'none';
// 强制触发重绘以重置动画
void el.offsetWidth;
// 应用新动画设置
el.style.animation = `custom-select-expand 0.1s ease-out ${direction} forwards`;
}
// #endregion
// #region /* 模拟输入按钮 */
let testBtn = document.querySelector("#beibeibeibei-panel-add-rule-dialog button.test");
testBtn.addEventListener('click', () => {
const dialog2 = testBtn.closest("dialog");
// 填充文本输入框
dialog2.querySelector('.rule-name').value = '旋转视频示例';
dialog2.querySelector('.url-pattern').value = 'yin.com';
dialog2.querySelector('.count-based-selector').value = 'video';
dialog2.querySelector('.action-selector').value = 'video';
dialog2.querySelector('.action-index').value = '0';
// 设置数量条件选择器为"最多"
const quantitySelector = dialog2.querySelector('.quantitySelector');
const quantityButton = quantitySelector.querySelector('.select-button');
quantityButton.textContent = '最多';
quantityButton.dataset.value = '3';
quantitySelector.dispatchEvent(new Event('change'));
// 填充数量值
dialog2.querySelector('.quantity-value').value = '1';
// 设置操作类型为"点击"
const actionTypeSelector = dialog2.querySelector('.action-type');
const actionButton = actionTypeSelector.querySelector('.select-button');
actionButton.textContent = '旋转';
actionButton.dataset.value = '2';
actionTypeSelector.dispatchEvent(new Event('change'));
// 填充操作数值
dialog2.querySelector('.action-num').value = '90'; // 新增字段
})
// #endregion
// #region /* 添加新条目 */
function collectRuleData() {
let dialog2 = document.querySelector("#beibeibeibei-panel-add-rule-dialog");
return {
ruleName: dialog2.querySelector('.rule-name').value.trim(),
urlPattern: dialog2.querySelector('.url-pattern').value.trim(),
selectorA: dialog2.querySelector('.count-based-selector').value.trim(),
quantityCondition: dialog2.querySelector('.quantitySelector .select-button').textContent,
quantityValue: getQuantityValues(dialog2),
selectorB: dialog2.querySelector('.action-selector').value.trim(),
actionIndex: dialog2.querySelector('.action-index').value.trim(),
actionType: dialog2.querySelector('.action-type .select-button').textContent,
actionNum: dialog2.querySelector('.action-num').value.trim()
};
}
function getQuantityValues(dialog) {
const condition = dialog.querySelector('.quantitySelector .select-button').dataset.value;
switch (condition) {
case '2': // 最少
case '3': // 最多
case '4': // 等于
return dialog.querySelector('.quantity-value').value;
case '5': // 范围
return `${dialog.querySelector('.min-quantity').value}-${dialog.querySelector('.max-quantity').value}`;
default:
return '';
}
}
function validateRuleData(data) {
if (!data.ruleName) {
alert("规则名称不能为空");
return false;
}
if (data.quantityCondition !== "无限制" && !data.selectorA) {
alert("选择器A不能为空");
return false;
}
if (!data.selectorB) {
alert("选择器B不能为空");
return false;
}
if (isNaN(data.actionIndex) || data.actionIndex < 0) {
alert("索引值必须是非负数字");
return false;
}
return true;
}
function addRuleToStorage(ruleData) {
const rules = GM_getValue('rulesData', []);
const newRule = [
ruleData.ruleName,
ruleData.urlPattern,
ruleData.selectorA,
formatQuantityCondition(ruleData.quantityCondition, ruleData.quantityValue),
ruleData.selectorB,
ruleData.actionIndex,
ruleData.actionType,
ruleData.actionNum
];
rules.push(newRule);
GM_setValue('rulesData', rules);
}
function formatQuantityCondition(condition, value) {
switch (condition) {
case '无限制': return '无限制';
case '最少': return `最少 ${value} 个`;
case '最多': return `最多 ${value} 个`;
case '等于': return `等于 ${value} 个`;
case '范围': return `范围 ${value} 个`;
default: return '无限制';
}
}
function addRuleToUI(ruleData) {
const ruleDialog = document.querySelector('#beibeibeibei-panel-rule-dialog');
const newBody = document.createElement('div');
newBody.className = 'column body';
const fields = [
ruleData.ruleName,
ruleData.urlPattern,
ruleData.selectorA,
formatQuantityCondition(ruleData.quantityCondition, ruleData.quantityValue),
ruleData.selectorB,
ruleData.actionIndex,
ruleData.actionType,
ruleData.actionNum
];
fields.forEach(text => {
const div = document.createElement('div');
div.textContent = text;
newBody.appendChild(div);
});
// 添加删除按钮
const deleteDiv = document.createElement('div');
const delBtn = document.createElement('button');
delBtn.className = 'remove';
delBtn.textContent = '删除此条';
delBtn.onclick = function () {
const bodyElement = this.closest('.column.body');
const index = bodyElement.dataset.index;
// 从页面中删除元素
bodyElement.remove();
// 从存储的数据中删除对应条目
const savedData = GM_getValue('rulesData', []);
savedData.splice(index, 1);
GM_setValue('rulesData', savedData);
};
deleteDiv.appendChild(delBtn);
newBody.appendChild(deleteDiv);
// 插入到添加按钮之前
const addBtn = ruleDialog.querySelector('.add');
ruleDialog.querySelector('.table').insertBefore(newBody, addBtn);
}
let addBtn = document.querySelector("#beibeibeibei-panel-add-rule-dialog button.add");
addBtn.addEventListener('click', () => {
const ruleData = collectRuleData();
if (validateRuleData(ruleData)) {
addRuleToStorage(ruleData);
addRuleToUI(ruleData);
addBtn.closest("dialog").close();
}
});
// #endregion
};
function beibeibeibei_socket() {
const startButton = document.querySelector("#beibeibeibei-panel .start");
const testButton = document.querySelector("#beibeibeibei-panel .test");
const portInput = document.querySelector("#beibeibeibei-panel input.port");
const pborder = document.querySelector(".beibeibeibei-panel-border");
const msgContainer = document.querySelector("#beibeibeibei-panel .msgs");
/** @type {HTMLElement} */
const led = document.querySelector("#beibeibeibei-panel .led");
// 获取端口号
const port = portInput.value.trim();
// 定义 WebSocket 的 URL
const wsUrl = `ws://localhost:${port}`;
// 定义 WebSocket 实例和定时器
/** @type {WebSocket} */
let ws;
let websocketCheckInterval;
let websocketCheckIntervalTime = 1000;
let messageCleanupInterval;
let messageCleanupIntervalTime = 5000;
// isrunning只是建议启动, 和 WebSocket 状态无关
let isrunning = false;
// 启动连接
startButton.addEventListener("click", () => {
isrunning = !isrunning;
// 修改UI
if (isrunning) {
startButton.textContent = '停止';
pborder.classList.add("running");
} else {
startButton.textContent = '启动';
pborder.classList.remove("running");
}
// 控制 Websocket
if (isrunning) {
// 如果 WebSocket 已经存在, 先关闭它
if (ws) {
ws.close();
}
// 创建新的 WebSocket 实例
ws = new WebSocket(wsUrl);
// 设置定时器, 间隔 intervalTimeout 毫秒检查一次 WebSocket 状态
websocketCheckInterval = setInterval(() => {
if (ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED) {
console.log("WebSocket 状态为 CLOSING 或 CLOSED, 尝试重新连接...");
appendMessage("info", `尝试重新连接`);
// 关闭当前 WebSocket 实例
ws.close();
// 创建新的 WebSocket 实例
ws = new WebSocket(wsUrl);
// 添加 WebSocket 的事件监听
ws.onopen = handleWebSocketOpen;
ws.onmessage = handleWebSocketMessage;
ws.onerror = handleWebSocketError;
ws.onclose = handleWebSocketClose;
}
}, websocketCheckIntervalTime);
// 设置定时器, 控制消息容器中的子元素数量
messageCleanupInterval = setInterval(() => {
const childrenCount = msgContainer.childElementCount;
if (childrenCount > 50) {
// 计算要删除的前 5% 子元素的数量
const deleteCount = Math.ceil(childrenCount * 0.05);
// 删除前 5% 的子元素
requestIdleCallback(() => {
for (let i = 0; i < deleteCount; i++) {
if (msgContainer.childElementCount > 0) {
msgContainer.removeChild(msgContainer.firstChild);
}
}
});
}
}, messageCleanupIntervalTime);
// 添加 WebSocket 的事件监听
ws.onopen = handleWebSocketOpen;
ws.onmessage = handleWebSocketMessage;
ws.onerror = handleWebSocketError;
ws.onclose = handleWebSocketClose;
} else {
// 停止定时器
if (websocketCheckInterval) {
clearInterval(websocketCheckInterval);
websocketCheckInterval = null;
}
// 停止定时器
if (messageCleanupInterval) {
clearInterval(messageCleanupInterval);
messageCleanupInterval = null;
}
// 关闭 WebSocket 连接
if (ws) {
ws.close();
ws = null;
}
console.log("WebSocket 连接已停止");
}
});
// WebSocket 连接成功
function handleWebSocketOpen() {
console.log("WebSocket 连接成功");
appendMessage("info", `已连接 WebSocket 服务器:${wsUrl}`);
led.classList.add("websocket-connected");
led.title = "连接状态: 已连接";
}
// WebSocket 收到消息
function handleWebSocketMessage(/** @type { MessageEvent } */event) {
console.log("收到消息:", event.data);
appendMessage("msg", event.data);
// 处理消息并执行规则
beibeibeibei_processRules(event.data);
}
// WebSocket 发生错误
function handleWebSocketError(event) {
console.log("WebSocket 发生错误:", event);
appendMessage("error", "WebSocket 连接错误");
}
// WebSocket 连接关闭
function handleWebSocketClose(event) {
console.log("WebSocket 已关闭");
appendMessage("info", `WebSocket 连接已断开`);
led.classList.remove("websocket-connected");
led.title = "连接状态: 未连接";
}
// 测试按钮点击事件
testButton.addEventListener("click", () => {
if (ws && ws.readyState === WebSocket.OPEN) {
// 发送测试消息
const testMessage = "测试消息";
ws.send(testMessage);
appendMessage("info", `已发送测试消息:${testMessage}`);
} else {
appendMessage("error", "WebSocket 连接未开启, 无法发送测试消息");
}
});
// 显示消息
function appendMessage(className, textContent) {
const msg = document.createElement("p");
msg.className = className;
msg.textContent = textContent;
msgContainer.appendChild(msg);
// 获取滚动条位置和容器高度信息
const isNearBottom = msgContainer.scrollTop + msgContainer.clientHeight >= msgContainer.scrollHeight * 0.9;
// 如果滚动条接近底部,则滚动到底部
if (isNearBottom) {
msgContainer.scrollTop = msgContainer.scrollHeight;
}
}
}
function runWhenDocumentReady(...funcs) {
const validFuncs = [...new Set(funcs)].filter(item => typeof item === 'function');
if (document.readyState === 'loading') {
validFuncs.forEach(func =>
document.addEventListener('DOMContentLoaded', func)
);
} else {
validFuncs.forEach(func => func());
}
}
// const gmStorage = {
// rulesData: [
// ['msg1', '', 'button', '无限制', '.testA', '0', '旋转', '90'],
// ['msg1', '', 'button', '无限制', '.testA', '0', '缩放', '1.1'],
// ['msg1', '', 'button', '无限制', '.testA', '0', '点击', ''],
// ['msg-test', '', '.test-element', '最少 6 个', '.test-element', '0', '旋转', '135']
// ]
// };
// 保存规则数据
function beibeibeibei_saveRulesData() {
const bodyElements = document.querySelectorAll('#beibeibeibei-panel-rule-dialog .column.body');
const savedData = [];
bodyElements.forEach((element, index) => {
const children = element.children;
const rowData = [];
for (let i = 0; i < children.length; i++) {
if (i === children.length - 1) {
// 跳过最后一个包含按钮的div
continue;
}
rowData.push(children[i].textContent.trim());
}
savedData.push(rowData);
});
GM_setValue('rulesData', savedData);
console.log('规则数据已保存');
}
// 恢复规则数据
function beibeibeibei_restoreRulesData() {
const savedData = GM_getValue('rulesData', []);
const headerElement = document.querySelector('#beibeibeibei-panel-rule-dialog .column.header');
const addageElement = document.querySelector('#beibeibeibei-panel-rule-dialog .add');
const bodyElements = document.querySelectorAll('#beibeibeibei-panel-rule-dialog .column.body');
// 清除现有的body元素
bodyElements.forEach(element => {
element.remove();
});
// 恢复保存的内容
savedData.forEach((rowData, index) => {
const newBody = document.createElement('div');
newBody.className = 'column body';
newBody.dataset.index = index;
if (rowData.length < 8) {
rowData = rowData.concat([null]);
}
rowData.forEach((data) => {
const div = document.createElement('div');
div.textContent = data || '';
newBody.appendChild(div);
});
// 添加删除按钮
const deleteDiv = document.createElement('div');
const delBtn = document.createElement('button');
delBtn.className = 'remove';
delBtn.textContent = '删除此条';
delBtn.type = 'button';
delBtn.onclick = function () {
const bodyElement = this.closest('.column.body');
const index = bodyElement.dataset.index;
// 从页面中删除元素
bodyElement.remove();
// 从存储的数据中删除对应条目
const savedData = GM_getValue('rulesData', []);
savedData.splice(index, 1);
GM_setValue('rulesData', savedData);
};
deleteDiv.appendChild(delBtn);
newBody.appendChild(deleteDiv);
// 插入到header和add按钮之间
headerElement.parentNode.insertBefore(newBody, addageElement);
});
}
// 执行规则
function beibeibeibei_processRules(message) {
// 获取所有保存的规则
const rules = GM_getValue('rulesData', []);
// 过滤规则
const filteredRules = rules.filter(item => {
// 确保 item 是数组且数组不为空
if (!Array.isArray(item) || item.length < 8) return false;
// 检查第 0 项是否匹配 message
const messageMatch = item[0] === message;
// 检查 URL 是否包含第 1 项
const urlMatch = urlContainsText(item[1]);
// 检查元素数量是否满足条件
const selector2 = item[2]; // 第2项是选择器
const condition = item[3]; // 第3项是条件
const elementCountMatch = checkElementCount(selector2, condition);
// 检查第4项选择器的数量是否大于等于第5项
const selector4 = item[4]; // 第4项是选择器
const selector4Count = parseInt(item[5]); // 第5项是第几项(从0开始)
const selector4CountMatch = checkElementCount(selector4, `最少 ${selector4Count + 1} 个`);
// 返回所有条件都满足的规则
// console.log({messageMatch, urlMatch, elementCountMatch, selector4CountMatch});
return messageMatch && urlMatch && elementCountMatch && selector4CountMatch;
});
console.log({ 0: 'beibeibeibei_processRules的filteredRules结果', message, filteredRules });
// 执行符合条件的规则
filteredRules.forEach((rule) => {
const elements = document.querySelectorAll(rule[4]);
if (elements.length > parseInt(rule[5])) {
const targetIndex = parseInt(rule[5]);
const targetElement = elements[targetIndex];
// 执行操作
switch (rule[6]) {
case '点击':{
if (typeof targetElement.click === 'function') {
targetElement.click();
} else {
targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
}
break;
}
case '旋转':{
const rotateValue = parseInt(rule[7]) || 0;
let targetElementRotate = parseInt(getComputedStyle(targetElement).rotate) || 0;
targetElement.style.rotate = `${(targetElementRotate + rotateValue) % 360}deg`;
break;
}
case '缩放':{
const scaleValue = rule[7];
targetElement.style.transform = `scale(${scaleValue})`;
break;
}
}
}
});
}
// 正则匹配当前网址
function urlMatchesPattern(pattern) {
const currentUrl = window.location.href;
const regex = new RegExp(pattern);
return regex.test(currentUrl);
}
// 普通文本匹配当前网址
function urlContainsText(text) {
const currentUrl = window.location.href;
return currentUrl.includes(text);
}
// 检查元素数量
function checkElementCount(selector, condition) {
if (condition === '无限制') {
// 无限制的意思就是无限制, 有没有元素都行, 选择器无效都行
return true;
}
// 验证选择器是否有效
if (!selector || typeof selector !== 'string' || selector.trim() === '') {
// console.error('无效的选择器:', selector);
return false; // 或抛出更明确的错误
}
// 获取匹配指定 CSS 选择器的元素个数
const elements = document.querySelectorAll(selector);
const count = elements.length;
// 解析条件
let min = 0;
let max = Infinity;
let exact = null;
let range = null;
// 正则表达式用于提取数字
const numberRegex = /\d+/g;
// 根据条件类型解析
if (condition === '无限制') {
// 无限制的意思就是无限制, 有没有元素都行
return true;
} else if (condition.startsWith('最少')) {
// 最少 N 个
const numbers = condition.match(numberRegex);
if (numbers && numbers.length > 0) {
min = parseInt(numbers[0]);
return count >= min;
}
} else if (condition.startsWith('最多')) {
// 最多 N 个
const numbers = condition.match(numberRegex);
if (numbers && numbers.length > 0) {
max = parseInt(numbers[0]);
return count <= max;
}
} else if (condition.startsWith('等于')) {
// 等于 N 个
const numbers = condition.match(numberRegex);
if (numbers && numbers.length > 0) {
exact = parseInt(numbers[0]);
return count === exact;
}
} else if (condition.startsWith('范围')) {
// 范围 A-B 个
const numbers = condition.match(numberRegex);
if (numbers && numbers.length === 2) {
min = parseInt(numbers[0]);
max = parseInt(numbers[1]);
// 如果颠倒, 自动调换
if (min > max) {
[min, max] = [max, min];
}
return count >= min && count <= max;
}
} else {
console.error('不支持的条件格式');
return false;
}
return false;
}
runWhenDocumentReady(add_HTML);
runWhenDocumentReady(beibeibeibei_socket, beibeibeibei_restoreRulesData, beibeibeibei_init);
runWhenDocumentReady(add_css);
// // 用于临时测试
// setTimeout(() => {
// // 添加测试目标元素
// const testArea = document.createElement("div");
// testArea.className = "test-area"; // 添加样式类
// testArea.id = "test-area";
// testArea.style.position = 'fixed';
// testArea.style.top = '200px';
// testArea.style.left = '200px';
// testArea.style.width = "300px";
// testArea.style.height = "200px";
// testArea.style.background = "linear-gradient(135deg,#FFD26F 15%,#3677FF 100%)";
// testArea.style.padding = "20px";
// testArea.style.display = "flex";
// testArea.style.flexWrap = "wrap";
// testArea.style.justifyContent = "flex-start";
// testArea.style.alignItems = "flex-start";
// document.body.appendChild(testArea);
// // 添加一些可点击、可旋转、可缩放的元素
// for (let i = 0; i < 6; i++) {
// const testElement = document.createElement("div");
// testElement.className = "test-element";
// testElement.style.margin = "10px";
// testElement.style.width = "80px";
// testElement.style.height = "60px";
// testElement.style.display = "flex";
// testElement.style.justifyContent = "center";
// testElement.style.alignItems = "center";
// testElement.style.backgroundColor = "#e0e0e0";
// testElement.style.borderRadius = "5px";
// testElement.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.2)";
// testElement.style.cursor = "pointer";
// testElement.style.fontSize = "14px";
// testElement.style.transition = "all 0.3s ease";
// testElement.innerHTML = `测试元素 ${i + 1}`;
// testElement.addEventListener("click", function () {
// console.log(`测试元素 ${i + 1} 被点击了`);
// });
// testArea.appendChild(testElement);
// }
// document.querySelector("#beibeibeibei-panel > div.titlebar > button.rule").click();
// beibeibeibei_processRules('msg-test');
// }, 40);
})();